#pragma once

#include <iostream>
#include <stdexcept>
#include <strsafe.h>

#include "Windows.h"

namespace dlloader
{
    class DLLoader
    {

    private:
        HMODULE _handle;
        std::string _pathToLib;
        std::string _allocClassSymbol;
        std::string _deleteClassSymbol;

    public:
        DLLoader(
            const std::string &allocClassSymbol = "allocator",
            const std::string &deleteClassSymbol = "deleter") : _handle(nullptr),
                                                                _allocClassSymbol(allocClassSymbol), _deleteClassSymbol(deleteClassSymbol)
        {
        }

        ~DLLoader() = default;

        int openLib(const std::string &pathToLib)
        {
            _pathToLib = pathToLib;
            if (!(_handle = LoadLibrary(_pathToLib.c_str())))
            {
                std::cerr << "Can't open and load " << _pathToLib << ", error code: " << last_error() << std::endl;
                return -1;
            }

            return 0;
        }

        template <class T>
        std::shared_ptr<T> getClassInstance()
        {
            using allocClass = T *(*)();
            using deleteClass = void (*)(T *);

            auto allocFunc = reinterpret_cast<allocClass>(
                GetProcAddress(_handle, _allocClassSymbol.c_str()));
            auto deleteFunc = reinterpret_cast<deleteClass>(
                GetProcAddress(_handle, _deleteClassSymbol.c_str()));

            if (!allocFunc || !deleteFunc)
            {
                //closeLib();
                std::cerr << "Can't find allocator or deleter symbol in " << _pathToLib << ", error code: " << last_error() << std::endl;
            }

            return std::shared_ptr<T>(
                allocFunc(),
                [deleteFunc](T *p)
                { deleteFunc(p); });
        }

        template <class T>
        std::shared_ptr<T> getFuncInstance(const std::string &func_name)
        {
            FARPROC func_ptr = ::GetProcAddress(_handle, func_name.c_str());
            if (func_ptr == nullptr)
            {
                std::cerr << "Can't find " << func_name << ", error code: " << last_error() << std::endl;
            }

            T *func = reinterpret_cast<T *>(func_ptr);
            return std::shared_ptr<T>(func, [](T *ptr) {});
        }

        void closeLib()
        {
            if (FreeLibrary(_handle) == 0)
            {
                std::cerr << "Can't close " << _pathToLib << ", error code: " << last_error() << std::endl;
            }
        }

        std::string last_error()
        {
            // https://msdn.microsoft.com/en-us/library/ms680582%28VS.85%29.aspx
            LPVOID lpMsgBuf;
            DWORD dw = ::GetLastError();

            ::FormatMessage(
                FORMAT_MESSAGE_ALLOCATE_BUFFER |
                    FORMAT_MESSAGE_FROM_SYSTEM |
                    FORMAT_MESSAGE_IGNORE_INSERTS,
                NULL,
                dw,
                MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                (LPTSTR)&lpMsgBuf,
                0, NULL);

            LPVOID lpDisplayBuf = (LPVOID)::LocalAlloc(LMEM_ZEROINIT,
                                                       (::lstrlen((LPCTSTR)lpMsgBuf) + 40) * sizeof(TCHAR));

            ::StringCchPrintf((LPTSTR)lpDisplayBuf,
                              ::LocalSize(lpDisplayBuf) / sizeof(TCHAR),
                              TEXT("Failed with error %d: %s"),
                              dw, lpMsgBuf);

            std::string err_str((LPCTSTR)lpDisplayBuf);

            ::LocalFree(lpMsgBuf);
            ::LocalFree(lpDisplayBuf);

            return err_str;
        }
    };

}